﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.RegularExpressions;
using BlogEngine.Core;
using BlogEngine.Core.Web.Controls;

/// <summary>
/// The <see cref="CatTagLinker"/>
/// class is an extension that automatically associates posts and pages to categories and links based on their content.
/// </summary>
[Extension("Automatically associates posts and pages with categories and tags based on their content", "2.0.0",
    "<a href='http://www.neovolve.com/' target='_blank'>Rory Primrose</a>")]
public class CatTagLinker
{
    /// <summary>
    /// Defines the prefix used to ignore matching of the particular value.
    /// </summary>
    /// <remarks>
    /// Ignored values are removed from the item before the item is saved.
    /// </remarks>
    private const String IgnoreSwitchPrefix = "-";

    /// <summary>
    /// Initializes a new instance of the <see cref="CatTagLinker"/> class.
    /// </summary>
    public CatTagLinker()
    {
        BusinessBase<Post, Guid>.Saving += ProcessEntry;
        BusinessBase<Page, Guid>.Saving += ProcessEntry;
    }

    /// <summary>
    /// Adds the missing categories.
    /// </summary>
    /// <param name="item">The item.</param>
    /// <param name="contents">The contents.</param>
    private static void AddMissingCategories(IPublishable item, String contents)
    {
        // Exit early if there are no categories to add
        if (Category.Categories == null)
        {
            return;
        }

        if (Category.Categories.Count == 0)
        {
            return;
        }

        // Exit if there is no contents to check
        if (String.IsNullOrEmpty(contents))
        {
            return;
        }

        // Loop through each available category
        for (Int32 index = 0; index < Category.Categories.Count; index++)
        {
            Category currentCategory = Category.Categories[index];

            // Check if there is a categories object
            if (item.Categories == null)
            {
                // There is no setter so we can't work with this item
                // Pages by design don't support categories
                continue;
            }

            // Check if this category is already assigned to the item
            if (item.Categories.Contains(currentCategory))
            {
                continue;
            }

            // Check if there is a negative/ignore switch for this category
            Boolean skipItem = false;

            // Loop through each category in the item
            for (Int32 itemIndex = 0; itemIndex < item.Categories.Count; itemIndex++)
            {
                Category itemCategory = item.Categories[itemIndex];

                // Check if this category on the item indicates that the category should be ignored from autolinking
                if (itemCategory.Title.Equals(
                    IgnoreSwitchPrefix + currentCategory.Title, StringComparison.OrdinalIgnoreCase))
                {
                    skipItem = true;

                    break;
                }
            }

            // Check if this item should be skipped
            if (skipItem)
            {
                continue;
            }

            // Use a regular expression to find the category as a whole word
            if (Regex.IsMatch(contents, @"(\b|(?<=\W))" + Regex.Escape(currentCategory.Title) + @"(\b|(?=\W))", RegexOptions.IgnoreCase))
            {
                // Add the category
                item.Categories.Add(currentCategory);
            }
        }
    }

    /// <summary>
    /// Adds the missing tags.
    /// </summary>
    /// <param name="post">The post.</param>
    /// <param name="contents">The contents.</param>
    /// <param name="availableTags">The available tags.</param>
    private static void AddMissingTags(Post post, String contents, IList<String> availableTags)
    {
        // Check that we have an object to work with
        if (post == null)
        {
            // Nothing to do
            return;
        }

        // Exit if there is no contents to check
        if (String.IsNullOrEmpty(contents))
        {
            return;
        }

        // Loop through the available tags
        for (Int32 index = 0; index < availableTags.Count; index++)
        {
            String tag = availableTags[index];

            // Check if the post is already associated with this tag
            if (post.Tags.Contains(tag))
            {
                continue;
            }

            // Check if there is a negative/ignore switch for this tag
            Boolean skipItem = false;

            // Loop through each tag in the item
            for (Int32 itemIndex = 0; itemIndex < post.Tags.Count; itemIndex++)
            {
                String itemTag = post.Tags[itemIndex];

                // Check if this tag on the item indicates that the tag should be ignored from autolinking
                if (itemTag.Equals(
                    IgnoreSwitchPrefix + tag, StringComparison.OrdinalIgnoreCase))
                {
                    skipItem = true;

                    break;
                }
            }

            // Check if this item should be skipped
            if (skipItem)
            {
                continue;
            }

            // Use a regular expression to find the tag as a whole word
            if (Regex.IsMatch(contents, @"(\b|(?<=\W))" + Regex.Escape(tag) + @"(\b|(?=\W))", RegexOptions.IgnoreCase))
            {
                // This post contains the tag in its contents, add it to the list
                post.Tags.Add(tag);
            }
        }
    }

    /// <summary>
    /// Gets the available tags.
    /// </summary>
    /// <returns>A list of <see cref="String"/> values.</returns>
    private static List<String> GetAvailableTags()
    {
        List<String> availableTags = new List<String>();

        // Loop through each post
        for (Int32 postIndex = 0; postIndex < Post.Posts.Count; postIndex++)
        {
            Post post = Post.Posts[postIndex];

            // Loop through each tag
            for (Int32 tagIndex = 0; tagIndex < post.Tags.Count; tagIndex++)
            {
                // Get the tag
                String tag = post.Tags[tagIndex];

                // Check if the tag has already been found
                if (availableTags.Contains(tag) == false)
                {
                    // This is a new tag, add it to the list
                    availableTags.Add(tag);
                }
            }
        }

        return availableTags;
    }

    /// <summary>
    /// Processes the entry.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="e">The <see cref="BlogEngine.Core.SavedEventArgs"/> instance containing the event data.</param>
    private static void ProcessEntry(Object sender, SavedEventArgs e)
    {
        if (e.Action == SaveAction.None
            || e.Action == SaveAction.Delete)
        {
            return;
        }

        IPublishable item = sender as IPublishable;

        // Check if the item is IPublishable
        if (item == null)
        {
            // This is not a type that this extension can work with
            return;
        }

        // Check if there is contents for the item
        if (String.IsNullOrEmpty(item.Content))
        {
            // Exit early as there is nothing to do
            return;
        }

        // Get the available tags
        List<String> availableTags = GetAvailableTags();

        // Add categories missing by checking the items native contents
        AddMissingCategories(item, item.Title);
        AddMissingCategories(item, item.Content);

        // Check if the item is a Post (rather than a Page or Comment - they don't support tags)
        if (item is Post)
        {
            // Add tags missing by checking the items native contents
            AddMissingTags(item as Post, item.Title, availableTags);
            AddMissingTags(item as Post, item.Content, availableTags);
        }

        // See if the snippet extension is available
        Type snippetExtension = Type.GetType("Snippets");

        // Check if the snippet extension is available
        if (snippetExtension != null)
        {
            MethodInfo formatSnippets = snippetExtension.GetMethod(
                "FormatContents", BindingFlags.Static | BindingFlags.Public | BindingFlags.InvokeMethod);

            // Defensive coding - A Snippets type may be available that isn't the Snippets extension we are expecting
            if (formatSnippets != null)
            {
                // Get the contents of the item formatted with snippets
                String snippetFormattedContents = formatSnippets.Invoke(
                                                      null,
                                                      new Object[]
                                                          {
                                                              item.Content
                                                          }) as String;

                // Add categories missing by checking the items native contents
                AddMissingCategories(item, snippetFormattedContents);

                // Check if the item is a Post (rather than a Page or Comment - they don't support tags)
                if (item is Post)
                {
                    // Add tags missing by checking the items native contents
                    AddMissingTags(item as Post, snippetFormattedContents, availableTags);
                }
            }
        }

        RemoveIgnoredCategories(item);

        if (item is Post)
        {
            RemoveIgnoredTags(item as Post);
        }
    }

    /// <summary>
    /// Removes the ignored categories.
    /// </summary>
    /// <param name="item">The item.</param>
    private static void RemoveIgnoredCategories(IPublishable item)
    {
        // Exit if there are no categories to process
        if (item.Categories == null)
        {
            return;
        }

        // Decrement through the categories of the item
        for (Int32 index = item.Categories.Count - 1; index >= 0; index--)
        {
            Category category = item.Categories[index];

            // Check if the category has been ignored for auto linking
            if (category.Title.StartsWith(IgnoreSwitchPrefix, StringComparison.OrdinalIgnoreCase))
            {
                // Remove this item
                item.Categories.Remove(category);
            }
        }
    }

    private static void RemoveIgnoredTags(Post post)
    {
        // Decrement through the tags of the item
        for (Int32 index = post.Tags.Count - 1; index >= 0; index--)
        {
            String tag = post.Tags[index];

            // Check if the tag has been ignored for auto linking
            if (tag.StartsWith(IgnoreSwitchPrefix, StringComparison.OrdinalIgnoreCase))
            {
                // Remove this item
                post.Tags.Remove(tag);
            }
        }
    }
}